import PrepareAndroid from './common/prepare-android.mdx';
import SetupEnv from './common/setup-env.mdx';

# 与 Android(adb) 集成

在使用 adb 连接 Android 设备后，你可以使用 Midscene javascript SDK 来控制 Android 设备。

import { PackageManagerTabs } from '@theme';

:::info 样例项目
使用 javascript SDK 控制 Android 设备：[https://github.com/web-infra-dev/midscene-example/blob/main/android/javascript-sdk-demo](https://github.com/web-infra-dev/midscene-example/blob/main/android/javascript-sdk-demo)

与 Vitest 集成和测试：[https://github.com/web-infra-dev/midscene-example/tree/main/android/vitest-demo](https://github.com/web-infra-dev/midscene-example/tree/main/android/vitest-demo)
:::

:::info 案例展示

[查看更多案例](./blog-support-android-automation.mdx)

<p align="center">
  <img src="/android.png" alt="android" width="400" />
</p>

:::

<PrepareAndroid />

<SetupEnv />

## 集成 Midscene

### 第一步：安装依赖

<PackageManagerTabs command="install @midscene/android --save-dev" />

### 第二步：编写脚本

这里以使用安卓浏览器搜索耳机为例。(当然，你也可以使用设备上的其他任何应用)

编写下方代码，保存为 `./demo.ts`

```typescript title="./demo.ts"
import {
  AndroidAgent,
  AndroidDevice,
  getConnectedDevices,
} from '@midscene/android';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Promise.resolve(
  (async () => {
    const devices = await getConnectedDevices();
    const page = new AndroidDevice(devices[0].udid);

    // 👀 初始化 Midscene agent
    const agent = new AndroidAgent(page, {
      aiActionContext:
        '如果出现位置、权限、用户协议等弹窗，点击同意。如果出现登录页面，关闭它。',
    });
    await page.connect();

    // 👀 打开浏览器并导航到 ebay.com（请确保当前页面有浏览器 App 喔）
    await agent.aiAction('open browser and navigate to ebay.com');

    await sleep(5000);

    // 👀 输入关键词，执行搜索
    await agent.aiAction('在搜索框输入 "Headphones" ，敲回车');

    // 👀 等待加载完成
    await agent.aiWaitFor('页面中至少有一个耳机商品');
    // 或者你也可以使用一个普通的 sleep:
    // await sleep(5000);

    // 👀 理解页面内容，提取数据
    const items = await agent.aiQuery(
      '{itemTitle: string, price: Number}[], 找到列表里的商品标题和价格',
    );
    console.log('耳机商品信息', items);

    // 👀 用 AI 断言
    await agent.aiAssert('界面左侧有类目筛选功能');
  })(),
);
```

### 第三步：运行

使用 `tsx` 来运行

```bash
# run
npx tsx demo.ts
```

稍等片刻，你会看到如下输出：

```log
[
 {
   itemTitle: 'JBL Tour Pro 2 - True wireless Noise Cancelling earbuds with Smart Charging Case',
   price: 551.21
 },
 {
   itemTitle: 'Soundcore Space One无线耳机40H ANC播放时间2XStronger语音还原',
   price: 543.94
 }
]
```

### 第四步：查看运行报告

当上面的命令执行成功后，会在控制台输出：`Midscene - report file updated: /path/to/report/some_id.html`， 通过浏览器打开该文件即可看到报告。

## 构造函数与接口

### `AndroidDevice` 的构造函数

AndroidDevice 的构造函数支持以下参数：

- `deviceId: string` - 设备 id
- `opts?: AndroidDeviceOpt` - 可选参数，用于初始化 AndroidDevice 的配置
  - `autoDismissKeyboard?: boolean` - 可选参数，是否在输入文本后自动关闭键盘。默认值为 true。
  - `keyboardDismissStrategy?: 'esc-first' | 'back-first'` - 可选参数，关闭键盘的策略。'esc-first' 优先尝试 ESC 键，如果键盘仍存在则尝试返回键。'back-first' 优先尝试返回键，如果键盘仍存在则尝试 ESC 键。默认值为 'esc-first'。
  - `androidAdbPath?: string` - 可选参数，用于指定 adb 可执行文件的路径。
  - `remoteAdbHost?: string` - 可选参数，用于指定远程 adb 主机。
  - `remoteAdbPort?: number` - 可选参数，用于指定远程 adb 端口。
  - `imeStrategy?: 'always-yadb' | 'yadb-for-non-ascii'` - 可选参数，控制 Midscene 何时调用 [yadb](https://github.com/ysbing/YADB) 来输入文本。默认值为 'always-yadb'。
  - `displayId?: number` - 可选参数，用于指定要使用的显示器 ID。默认值为 undefined，表示使用当前显示器。

### Android Agent 上的更多接口

除了 [API 参考](./api.mdx) 中的通用 Agent 接口，AndroidAgent 还提供了一些其他接口：

#### `agent.launch()`

启动一个网页或原生页面。

- 类型

```typescript
function launch(uri: string): Promise<void>;
```

- 参数：

  - `uri: string` - 要打开的 uri，可以是网页 url 或原生 app 的 package name 或 activity name，如果存在 activity name，则以 / 分隔（例如：com.android.settings/.Settings）

- 返回值：

  - `Promise<void>`

- 示例：

```typescript
import { AndroidAgent, AndroidDevice } from '@midscene/android';

const page = new AndroidDevice('s4ey59');
const agent = new AndroidAgent(page);

await agent.launch('https://www.ebay.com'); // 打开网页
await agent.launch('com.android.settings'); // 打开系统设置 app(package name)
await agent.launch('com.android.settings/.Settings'); // 打开系统设置 app(package name) 的 .Settings(activity name) 页面
```

#### `agentFromAdbDevice()`

从已连接的 adb 设备中，创建一个 AndroidAgent。

- 类型

```typescript
function agentFromAdbDevice(
  deviceId?: string,
  opts?: PageAgentOpt,
): Promise<AndroidAgent>;
```

- 参数：

  - `deviceId?: string` - 可选参数，要连接的 adb 设备 id，如果未传入，则使用第一个连接的设备
  - `opts?: PageAgentOpt & AndroidDeviceOpt` - 可选参数，用于初始化 AndroidAgent 的配置，其中 PageAgentOpt 参考 [构造器](./api.mdx)，AndroidDeviceOpt 的配置值参考 [AndroidDevice 的构造函数](./integrate-with-android#androiddevice-%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0)

- 返回值：

  - `Promise<AndroidAgent>` 返回一个 AndroidAgent 实例

- 示例：

```typescript
import { agentFromAdbDevice } from '@midscene/android';

const agent = await agentFromAdbDevice('s4ey59'); // 传入 deviceId
const agent = await agentFromAdbDevice(); // 不传入 deviceId，则使用第一个连接的设备
```

#### `getConnectedDevices()`

获取所有连接的 Android 设备。

- 类型

```typescript
function getConnectedDevices(): Promise<Device[]>;
interface Device {
  /**
   * The device udid.
   */
  udid: string;
  /**
   * Current device state, as it is visible in
   * _adb devices -l_ output.
   */
  state: string;
  port?: number;
}
```

- 返回值：

  - `Promise<Device[]>` 返回一个 Device 数组

- 示例：

```typescript
import { agentFromAdbDevice, getConnectedDevices } from '@midscene/android';

const devices = await getConnectedDevices();
console.log(devices);
const agent = await agentFromAdbDevice(devices[0].udid);
```

## 扩展自定义交互动作

使用 `customActions` 选项，结合 `defineAction` 定义的自定义交互动作，可以扩展 Agent 的动作空间。这些动作会追加在内置动作之后，方便 Agent 在规划阶段调用。

```typescript
import { getMidsceneLocationSchema, z } from '@midscene/core';
import { defineAction } from '@midscene/core/device';
import { AndroidAgent, AndroidDevice } from '@midscene/android';

const ContinuousClick = defineAction({
  name: 'continuousClick',
  description: 'Click the same target repeatedly',
  paramSchema: z.object({
    locate: getMidsceneLocationSchema(),
    count: z
      .number()
      .int()
      .positive()
      .describe('How many times to click'),
  }),
  async call(param) {
    const { locate, count } = param;
    console.log('click target center', locate.center);
    console.log('click count', count);
    // 在这里结合 locate + count 实现自定义点击逻辑
  },
});

const page = new AndroidDevice('your-device-id');
const agent = new AndroidAgent(page, {
  customActions: [ContinuousClick],
});

await agent.aiAction('点击红色按钮五次');
```

更多关于自定义动作的细节，请参考 [集成到任意界面](./integrate-with-any-interface)。

## 更多

- 更多 Agent 上的 API 接口请参考 [API 参考](./api.mdx)。
- 更多关于提示词的技巧请参考 [提示词技巧](./prompting-tips)

## FAQ

### 为什么我连接了设备，但是通过 adb 仍然无法控制？

请检查是否在系统设置的开发者选项中，如果存在『USB 调试（安全设置）』，也需要开启。

<p align="center">
  <img src="/android-usb-debug.png" alt="android usb debug" width="400" />
</p>

### 如何使用自定义的 adb 路径、远程 adb 主机和端口？

你可以使用 `MIDSCENE_ADB_PATH` 环境变量来指定 adb 可执行文件的路径，`MIDSCENE_ADB_REMOTE_HOST` 环境变量来指定远程 adb 主机，`MIDSCENE_ADB_REMOTE_PORT` 环境变量来指定远程 adb 端口。

```bash
export MIDSCENE_ADB_PATH=/path/to/adb
export MIDSCENE_ADB_REMOTE_HOST=192.168.1.100
export MIDSCENE_ADB_REMOTE_PORT=5037
```

此外，也可以通过 AndroidDevice 的构造函数来指定 adb 可执行文件的路径、远程 adb 主机和端口。

```typescript
const device = new AndroidDevice('s4ey59', {
  androidAdbPath: '/path/to/adb',
  remoteAdbHost: '192.168.1.100',
  remoteAdbPort: 5037,
});
```
